package cn.jtfadmin.base.shiro.config;

import cn.jtfadmin.base.lang.common.utils.RequestUtils;
import cn.jtfadmin.base.lang.constant.SecurityConstant;
import cn.jtfadmin.base.shiro.support.ShiroUserHoler;
import cn.jtfadmin.base.shiro.support.*;
import org.apache.shiro.authc.AbstractAuthenticator;
import org.apache.shiro.authc.AuthenticationListener;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authz.Authorizer;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.mgt.*;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionFactory;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.config.AbstractShiroConfiguration;
import org.apache.shiro.spring.config.ShiroBeanConfiguration;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.util.AntPathMatcher;
import org.apache.shiro.util.PatternMatcher;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.util.StringUtils;

import javax.cache.CacheManager;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;

import static org.apache.shiro.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR;


/**
 * shiro配置,配置构建SessionsSecurityManager
 *
 * @author jtf
 * @since 0.0.1
 */
@Configuration
@Import(ShiroBeanConfiguration.class)
public class ShiroAutoConfiguration extends AbstractShiroConfiguration {


    /**
     * @see DefaultFilterDefinition
     */
    @Autowired
    protected ObjectProvider<FilterChainDefinition> filterChainDefinitionList;

    @Autowired
    protected ObjectProvider<AuthenticationListener> authenticationListeners;


    @Bean
    protected ShiroCacheManager shiroCacheManager(CacheManager cacheManager){
        return new ShiroCacheManager(cacheManager);
    }

    @Bean
    protected FilterChainDefinition defaultFilterChainDefinition(){
        return new DefaultFilterDefinition();
    }

    @Bean
    @Override
    protected RememberMeManager rememberMeManager() {
        CacheRememberMeManager cacheRememberMeManager = new CacheRememberMeManager();
        cacheRememberMeManager.setCacheManager(cacheManager);
        return cacheRememberMeManager;
    }


    // 原型模式， 当获取后重新生成
     @Bean()
     @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        List<FilterChainDefinition> filterChainDefinitions = new ArrayList<>();
        filterChainDefinitionList.forEach(t->filterChainDefinitions.add(t));
         PatternMatcher patternMatcher = new AntPathMatcher();
        Map<String, Map<String, Set<String>>> f = new LinkedHashMap<>(); // url-> (filter->content)
        for (FilterChainDefinition filterChainDefinition : filterChainDefinitions) {
            LinkedHashMap<String, String> filterChainDefinitionMap = filterChainDefinition.getFilterChainDefinitionMap();
            for (Map.Entry<String, String> deMapEntry : filterChainDefinitionMap.entrySet()) {
                String url = deMapEntry.getKey();
                if (url != null && !DEFAULT_PATH_SEPARATOR.equals(url)
                        && url.endsWith(DEFAULT_PATH_SEPARATOR)) {
                    url = url.substring(0, url.length() - 1);
                }
                String value = deMapEntry.getValue();
                /**
                 * Splits the comma-delimited filter chain definition line into individual filter definition tokens.
                 * <p/>
                 * Example Input:
                 * <pre>
                 *     foo, bar[baz], blah[x, y]
                 * </pre>
                 * Resulting Output:
                 * <pre>
                 *     output[0] == foo
                 *     output[1] == bar[baz]
                 *     output[2] == blah[x, y]
                 * </pre>
                 * @param chainDefinition the comma-delimited filter chain definition.
                 * @return an array of filter definition tokens
                 * @since 1.2
                 * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
                 */
                String[] split = org.apache.shiro.util.StringUtils.split(value, org.apache.shiro.util.StringUtils.DEFAULT_DELIMITER_CHAR, '[', ']', true, true);
                boolean exist = false;
                for (Map.Entry<String, Map<String, Set<String>>> fEntry : f.entrySet()) {
                    String key = fEntry.getKey();
                    Map<String, Set<String>> fiterNameContent = fEntry.getValue();
                    if (key != null && !DEFAULT_PATH_SEPARATOR.equals(key)
                            && key.endsWith(DEFAULT_PATH_SEPARATOR)) {
                        key = key.substring(0, key.length() - 1);
                    }
                    if(Objects.equals(url, key)){
                        exist = true;
                    }
                    if(patternMatcher.matches(url, key)){
                        for (String token : split) {
                            String[] strings = toNameConfigPair(token);
                            if(fiterNameContent.containsKey(strings[0])){
                                if(StringUtils.hasText(strings[1])){
                                    fiterNameContent.get(strings[0]).addAll(StringUtils.commaDelimitedListToSet(strings[1]));
                                }
                            }else {
                                if(StringUtils.hasText(strings[1])){
                                    fiterNameContent.put(strings[0],StringUtils.commaDelimitedListToSet(strings[1]));
                                }else {
                                    fiterNameContent.put(strings[0], new LinkedHashSet<>());
                                }
                            }
                        }
                    }

                }
                if(!exist){
                    Map<String, Set<String>> m = new LinkedHashMap<>();
                    for (String token : split) {
                        String[] strings = toNameConfigPair(token);
                        m.put(strings[0], StringUtils.commaDelimitedListToSet(strings[1]));
                    }
                    f.put(url, m);
                }

            }
        }
        Map<String, String> linkedMap = new LinkedHashMap<>();
        f.forEach((k,v)->{
            List<String> list = new ArrayList<>();
            v.forEach((k1,v1)->{
                if(v1.size()==0){
                    list.add(k1);
                }else {
                    list.add(k1+"["+StringUtils.collectionToCommaDelimitedString(v1)+"]");
                }
            });
            linkedMap.put(k, StringUtils.collectionToCommaDelimitedString(list));

        });
         chainDefinition.addPathDefinitions(linkedMap);
        return chainDefinition;
    }
    /**
     * Based on the given filter chain definition token (e.g. 'foo' or 'foo[bar, baz]'), this will return the token
     * as a name/value pair, removing any brackets as necessary.  Examples:
     * <table>
     *     <tr>
     *         <th>Input</th>
     *         <th>Result</th>
     *     </tr>
     *     <tr>
     *         <td>{@code foo}</td>
     *         <td>returned[0] == {@code foo}<br/>returned[1] == {@code null}</td>
     *     </tr>
     *     <tr>
     *         <td>{@code foo[bar, baz]}</td>
     *         <td>returned[0] == {@code foo}<br/>returned[1] == {@code bar, baz}</td>
     *     </tr>
     * </table>
     * @param token the filter chain definition token
     * @return A name/value pair representing the filter name and a (possibly null) config value.
     * @throws ConfigurationException if the token cannot be parsed
     * @since 1.2
     * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
     */
    protected String[] toNameConfigPair(String token) throws ConfigurationException {

        try {
            String[] pair = token.split("\\[", 2);
            String name = org.apache.shiro.util.StringUtils.clean(pair[0]);

            if (name == null) {
                throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token);
            }
            String config = null;

            if (pair.length == 2) {
                config = org.apache.shiro.util.StringUtils.clean(pair[1]);
                //if there was an open bracket, it assumed there is a closing bracket, so strip it too:
                config = config.substring(0, config.length() - 1);
                config = org.apache.shiro.util.StringUtils.clean(config);

                //backwards compatibility prior to implementing SHIRO-205:
                //prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets
                //if that config required commas.  We need to strip those quotes to get to the interior quoted definition
                //to ensure any existing quoted definitions still function for end users:
                if (config != null && config.startsWith("\"") && config.endsWith("\"")) {
                    String stripped = config.substring(1, config.length() - 1);
                    stripped = org.apache.shiro.util.StringUtils.clean(stripped);

                    //if the stripped value does not have any internal quotes, we can assume that the entire config was
                    //quoted and we can use the stripped value.
                    if (stripped != null && stripped.indexOf('"') == -1) {
                        config = stripped;
                    }
                    //else:
                    //the remaining config does have internal quotes, so we need to assume that each comma delimited
                    //pair might be quoted, in which case we need the leading and trailing quotes that we stripped
                    //So we ignore the stripped value.
                }
            }

            return new String[]{name, config};

        } catch (Exception e) {
            String msg = "Unable to parse filter chain definition token: " + token;
            throw new ConfigurationException(msg, e);
        }
    }


    @Bean
    @Override
    protected SessionManager sessionManager() {
        TokenWebSessionManager webSessionManager = new TokenWebSessionManager();
        webSessionManager.setSessionFactory(sessionFactory());
        webSessionManager.setSessionDAO(sessionDAO());
        webSessionManager.setDeleteInvalidSessions(sessionManagerDeleteInvalidSessions);
        return webSessionManager;
    }

    @Bean
    @Override
    protected SessionDAO sessionDAO() {
        return new MemorySessionDAO(){
            @Override
            protected Serializable generateSessionId(Session session) {
                String header = RequestUtils.getRequest().getHeader(SecurityConstant.AUTH_HEADER_NAME);
                if(StringUtils.hasText(header)){
                    return header;
                }
                return super.generateSessionId(session);
            }
        };

    }



    @Bean
    @Override
    protected SubjectDAO subjectDAO() {
        return super.subjectDAO();
    }

    @Bean
    @Override
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        return super.sessionStorageEvaluator();
    }

    @Bean
    @Override
    protected SessionFactory sessionFactory() {
        return super.sessionFactory();
    }



    @Bean
    @Override
    protected SubjectFactory subjectFactory() {
        return new DefaultWebSubjectFactory();
    }

    @Bean
    @Override
    protected Authorizer authorizer() {
        return super.authorizer();
    }

    @Bean
    @Override
    protected AuthenticationStrategy authenticationStrategy() {
        return super.authenticationStrategy();
    }

    @Bean
    @Override
    protected Authenticator authenticator() {
        Authenticator authenticator = super.authenticator();
        if(authenticator instanceof AbstractAuthenticator){
            Set<AuthenticationListener> collect = authenticationListeners.stream().collect(Collectors.toSet());
            ((AbstractAuthenticator) authenticator).setAuthenticationListeners(collect);
        }
        return authenticator;
    }

    protected SessionsSecurityManager createSecurityManager() {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSubjectDAO(subjectDAO());
        securityManager.setSubjectFactory(subjectFactory());
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    @Bean
    @Override
    protected SessionsSecurityManager securityManager(List<Realm> realms) {

        return super.securityManager(realms);
    }



    @Bean
    public Realm defaultRealm(List<UserBuilder> userBuilders) {
        return new DefaultRealm(userBuilders);
    }


    @Bean
    public ShiroUserHoler shiroUserHoler(List<UserBuilder> userBuilders){
        return new ShiroUserHoler(userBuilders);
    }


    @Bean
    public AuthenticationListener defaultAuthenticationListener(){
        return new DefaultAuthenticationListener();
    }

}
